<script lang="jsx">
import {
  computed,
  defineComponent,
  getCurrentInstance,
  reactive,
  ref,
  nextTick,
  watch,
  onMounted,
  provide,
  onBeforeUnmount,
  withModifiers,
  resolveComponent
} from 'vue'
import elementResizeDetectorMaker from 'element-resize-detector'
import globalConfig from '../../mixins/globalConfig.js'

const isClient = typeof window !== 'undefined'


export default defineComponent({
  name: 'Tabs',
  mixins: [globalConfig],
  emits: ['on-click', 'on-dblclick', 'on-contextmenu', 'on-tab-remove', 'on-drag-drop', 'update:modelValue'],
  props: {
    modelValue: {
      type: [String, Number],
    },
    type: {
      validator(value) {
        return ['line', 'card'].includes(value)
      },
      default: 'line',
    },
    size: {
      validator(value) {
        return ['small', 'default'].includes(value)
      },
      default: 'default',
    },
    animated: {
      type: Boolean,
      default: true,
    },
    captureFocus: {
      type: Boolean,
      default: false,
    },
    closable: {
      type: Boolean,
      default: false,
    },
    beforeRemove: Function,
    // Tabs 嵌套时，用 name 区分层级
    name: {
      type: String,
    },
    // 待实现
    draggable: {
      type: Boolean,
      default: false,
    },
    // 待实现
    autoCloseContextmenu: {
      type: Boolean,
      default: true,
    },
  },
  setup(props, { emit, slots, attrs, expose }) {
    const instance = getCurrentInstance()
    const globalConfig = instance.proxy.globalConfig

    const Icon = resolveComponent('e-icon')

    const Dropdown = resolveComponent('e-dropdown');
    const DropdownMenu = resolveComponent('e-dropdown-menu');

    const prefixCls = 'haloe-tabs'
    const transitionTime = 300

    const activeKey = ref(null)
    const focusedKey = ref(null)
    const navList = ref([])
    const paneList = []
    const tableList = reactive([])
    const transitioning = ref(false)

    let observer = null
    let mutationObserver = null

    const tabsWrap = ref(null)
    const navContainer = ref(null)
    const navWrap = ref(null)
    const nav = ref(null)
    const panes = ref(null)
    const navScroll = ref(null)

    const navStyle = reactive({
      transform: '',
    })

    const barWidth = ref(0)
    const barOffset = ref(0)

    const barStyle = computed(function () {
      let style = {
        visibility: 'hidden',
        width: `${barWidth.value}px`,
      }
      if (props.type === 'line') {
        style.visibility = 'visible'
      }
      if (props.animated) {
        style.transform = `translate3d(${barOffset.value}px, 0px, 0px)`
      } else {
        style.left = `${barOffset.value}px`
      }

      return style
    })

    const scrollable = ref(false)

    const _showSlot = ref(!!slots.extra);
    const showSlot = computed({
      get() {
        return _showSlot.value;
      },
      set(val) {
        _showSlot.value = val
      }
    })

    const classes = computed(function () {
      return [
        prefixCls,
        {
          [`${prefixCls}-card`]: props.type === 'card',
          [`${prefixCls}-mini`]:
            props.size === 'small' && props.type === 'line',
          [`${prefixCls}-no-animation`]: !props.animated
        },
      ]
    })

    const barClasses = computed(function () {
      return [
        `${prefixCls}-ink-bar`,
        {
          [`${prefixCls}-ink-bar-animated`]: props.animated,
        },
      ]
    })

    const contentClasses = computed(function () {
      return [
        `${prefixCls}-content`,
        {
          [`${prefixCls}-content-animated`]: props.animated,
        },
      ]
    })

    const contentStyle = computed(function () {
      const x = getTabIndex(activeKey.value)
      const p = x === 0 ? '0%' : `-${x}00%`

      let style = {}
      if (x > -1) {
        style = {
          transform: `translateX(${p}) translateZ(0px)`,
        }
      }
      return style
    })

    const contextMenuStyles = reactive({
      top: 0,
      left: 0,
    })

    const contextMenuVisible = ref(false)

    //global setting customArrow 有值时，arrow 赋值空
    const arrowType = computed(function () {
      if (globalConfig?.tabs?.customCloseIcon) {
        return ''
      }

      return globalConfig?.tabs?.closeIcon || 'cross'
    })

    const customArrowType = computed(function () {
      return (globalConfig?.tabs?.customCloseIcon || '') + ''
    })

    const arrowSize = computed(function () {
      return (globalConfig?.tabs?.closeIconSize || '') + ''
    })

    function getTabIndex(name) {
      return navList.value.findIndex((nav) => nav.name === name)
    }

    function getNextTab(list, activeKey, direction, countDisabledAlso) {
      const currentIndex = list.findIndex((tab) => tab.name === activeKey)
      const nextIndex = (currentIndex + direction + list.length) % list.length
      const nextTab = list[nextIndex]
      if (nextTab.disabled) {
        return getNextTab(list, nextTab.name, direction, countDisabledAlso)
      } else {
        return nextTab
      }
    }

    function focusFirst(element, root) {
      try {
        element.focus()
      } catch (err) {
        //todo
      }

      if (document.activeElement === element && element !== root) {
        return true
      }

      const candidates = element.children
      for (let candidate of candidates) {
        if (focusFirst(candidate, root)) {
          return true
        }
      }
      return false
    }

    function getTabs() {
      const AllTabPanes = paneList.map((item) => item.pane)
      const TabPanes = []

      AllTabPanes.forEach((item) => {
        if (item.tab && props.name) {
          if (item.tab === props.name) {
            TabPanes.push(item)
          }
        } else {
          TabPanes.push(item)
        }
      })

      // 在 TabPane 使用 v-if 时，并不会按照预先的顺序渲染，这时可设置 index，并从小到大排序
      TabPanes.sort((a, b) => {
        if (a.index && b.index) {
          return a.index > b.index ? 1 : -1
        }
      })
      return TabPanes
    }

    function updateNav() {
      navList.value = []
      getTabs().forEach((pane, index) => {
        navList.value.push({
          labelType: typeof pane.label,
          label: pane.label,
          icon: pane.icon || '',
          name: pane.currentName || index,
          disabled: pane.disabled,
          closable: pane.closable,
          contextMenu: pane.contextMenu,
        })
        if (!pane.currentName) {
          pane.currentName = index
        }
        if (index === 0) {
          if (!activeKey.value) {
            activeKey.value = pane.currentName || index
          }
        }
      })
      updateStatus()
      updateBar()
    }

    function updateStatus() {
      const tabs = getTabs()
      tabs.forEach((tab) => {
        tab.show = tab.currentName === activeKey.value || props.animated
      })
    }

    function updateBar() {
      nextTick(function () {
        const index = getTabIndex(activeKey.value)
        if (!nav.value) {
          return // 页面销毁时，这里会报错，为了解决 #2100
        }
        const prevTabs = nav.value.querySelectorAll(`.${prefixCls}-tab`)
        const tab = prevTabs[index]
        barWidth.value = tab ? parseFloat(tab.offsetWidth) : 0

        if (index > 0) {
          let offset = 0
          const gutter = props.size === 'small' ? 0 : 16
          for (let i = 0; i < index; i++) {
            offset += parseFloat(prevTabs[i].offsetWidth) + gutter
          }

          barOffset.value = offset
        } else {
          barOffset.value = 0
        }
        updateNavScroll()
      })
    }

    function updateStatus() {
      const tabs = getTabs()
      tabs.forEach((tab) => {
        tab.show = tab.currentName === activeKey.value || props.animated
      })
    }

    function getCurrentScrollOffset() {
      return navStyle.transform
        ? Number(navStyle.transform.match(/translateX\(-(\d+(\.\d+)*)px\)/)[1])
        : 0
    }

    /**
     * @param {number} value
     */
    function setOffset(value) {
      navStyle.transform = `translateX(-${value}px)`
    }

    function updateNavScroll() {
      const navWidth = nav.value.offsetWidth
      const containerWidth = navScroll.value.offsetWidth
      const currentOffset = getCurrentScrollOffset()
      if (containerWidth < navWidth) {
        scrollable.value = true
        if (navWidth - currentOffset < containerWidth) {
          setOffset(navWidth - containerWidth)
        }
      } else {
        scrollable.value = false
        if (currentOffset > 0) {
          setOffset(0)
        }
      }
    }

    function handleDrag(index, event) {
      const nav = navList.value[index]
      if (nav) {
        event.dataTransfer.setData('tab-name', nav.name)
      }
    }

    function handleDrop(index, event) {
      const nav = navList.value[index]
      if (nav) {
        const dragName = event.dataTransfer.getData('tab-name')
        event.preventDefault()

        let navNames = navList.value.map((item) => item.name)
        const a = parseInt(navNames.findIndex((item) => item === dragName))
        const b = parseInt(navNames.findIndex((item) => item === nav.name))
        navNames.splice(b, 1, ...navNames.splice(a, 1, navNames[b]))
        emit('on-drag-drop', dragName, nav.name, a, b, navNames)
      }
    }

    function closeContextMenu() {
      handleClickContextMenuOutside()
    }

    function handleTabKeyNavigation(e) {
      if (e.keyCode !== 37 && e.keyCode !== 39) {
        return
      }
      const direction = e.keyCode === 39 ? 1 : -1
      const nextTab = getNextTab(navList.value, focusedKey.value, direction)
      focusedKey.value = nextTab.name
    }

    function handleTabKeyboardSelect(init = false) {
      if (init) {
        return
      }
      const focused = focusedKey.value || 0
      const index = getTabIndex(focused)
      handleChange(index)
    }

    function scrollPrev() {
      const containerWidth = navScroll.value?.offsetWidth
      const currentOffset = getCurrentScrollOffset()

      if (!currentOffset) {
        return
      }
      let newOffset =
        currentOffset > containerWidth ? currentOffset - containerWidth : 0

      setOffset(newOffset)
    }

    function scrollNext() {
      const navWidth = nav.value?.offsetWidth
      const containerWidth = navScroll.value?.offsetWidth
      const currentOffset = getCurrentScrollOffset()
      if (navWidth - currentOffset <= containerWidth) {
        return
      }
      let newOffset =
        navWidth - currentOffset > containerWidth * 2
          ? currentOffset + containerWidth
          : navWidth - containerWidth

      setOffset(newOffset)
    }

    function handleScroll(e) {
      e.preventDefault()
      e.stopPropagation()
      const type = e.type
      let delta = 0
      if (type === 'DOMMouseScroll' || type === 'mousewheel') {
        delta = e.wheelDelta ? e.wheelDelta : -(e.detail || 0) * 40
      }
      if (delta > 0) {
        scrollPrev()
      } else {
        scrollNext()
      }
    }

    function tabCls(item) {
      return [
        `${prefixCls}-tab`,
        {
          [`${prefixCls}-tab-disabled`]: item.disabled,
          [`${prefixCls}-tab-active`]: item.name === activeKey.value,
          [`${prefixCls}-tab-focused`]: item.name === focusedKey.value,
        },
      ]
    }

    function scrollToActiveTab() {
      if (!scrollable.value) {
        return
      }

      const activeTab = instance.proxy.$el.querySelector(
        `.${prefixCls}-tab-active`
      )
      if (!activeTab) {
        return
      }

      const activeTabBounding = activeTab.getBoundingClientRect()
      const navScrollBounding = navScroll.value.getBoundingClientRect()
      const navBounding = nav.value.getBoundingClientRect()
      const currentOffset = getCurrentScrollOffset()
      let newOffset = currentOffset

      if (navBounding.right < navScrollBounding.right) {
        newOffset = nav.value.offsetWidth - navScrollBounding.width
      }

      if (activeTabBounding.left < navScrollBounding.left) {
        newOffset =
          currentOffset - (navScrollBounding.left - activeTabBounding.left)
      } else if (activeTabBounding.right > navScrollBounding.right) {
        newOffset =
          currentOffset + activeTabBounding.right - navScrollBounding.right
      }

      if (currentOffset !== newOffset) {
        setOffset(Math.max(newOffset, 0))
      }
    }

    function isInsideHiddenElement() {
      if (!isClient) {
        return
      }
      let parentNode = instance.proxy.$el.parentNode
      while (parentNode && parentNode !== document.body) {
        if (parentNode.style && parentNode.style.display === 'none') {
          return parentNode
        }
        parentNode = parentNode.parentNode
      }
      return false
    }

    function updateVisibility(index) {
      ;[...panes.value.querySelectorAll(`.${prefixCls}-tabpane`)].forEach(
        (el, i) => {
          if (index === i) {
            ;[...el.children]
              .filter((child) =>
                child.classList.contains(`${prefixCls}-tabpane`)
              )
              .forEach((child) => {
                child.style.visibility = 'visible'
              })
            if (props.captureFocus) {
              setTimeout(() => focusFirst(el, el), transitionTime)
            }
          } else {
            setTimeout(() => {
              ;[...el.children]
                .filter((child) =>
                  child.classList.contains(`${prefixCls}-tabpane`)
                )
                .forEach((child) => (child.style.visibility = 'hidden'))
            }, transitionTime)
          }
        }
      )
    }

    function handleResize() {
      updateNavScroll()
    }

    function handleChange(index) {
      if (transitioning.value) {
        return
      }

      transitioning.value = true
      setTimeout(() => {
        transitioning.value = false
      }, transitionTime)

      const nav = navList.value[index]
      if (!nav || nav.disabled) {
        return
      }
      activeKey.value = nav.name
      emit('update:modelValue', nav.name)
      emit('on-click', nav.name)
    }

    function handleDblclick(index) {
      const nav = navList.value[index]
      if (!nav || nav.disabled) {
        return
      }
      emit('on-dblclick', nav.name)
    }

    function handleContextmenu(index, event) {
      if (contextMenuVisible.value) {
        handleClickContextMenuOutside()
      }
      nextTick(() => {
        const nav = navList.value[index]
        if (!nav || nav.disabled || !nav.contextMenu) {
          return
        }

        event.preventDefault()
        const $TabsWrap = tabsWrap.value
        const TabsBounding = $TabsWrap.getBoundingClientRect()
        const position = {
          left: `${event.clientX - TabsBounding.left}px`,
          top: `${event.clientY - TabsBounding.top}px`,
        }
        contextMenuStyles.top = position.top
        contextMenuStyles.left = position.left

        contextMenuVisible.value = true
        emit('on-contextmenu', nav, event, position)
      })
    }

    function handleRemove(index) {
      if (!props.beforeRemove) {
        return handleRemoveTab(index)
      }

      const before = props.beforeRemove(index)

      if (before && before.then) {
        before.then(() => {
          handleRemoveTab(index)
        })
      } else {
        handleRemoveTab(index)
      }
    }

    function handleRemoveTab(index) {
      const tabs = getTabs()
      const tab = tabs[index]

      if (tab.currentName === activeKey.value) {
        const newTabs = getTabs()
        let activeK = -1

        if (newTabs.length) {
          const leftNoDisabledTabs = tabs.filter(
            (item, itemIndex) => !item.disabled && itemIndex < index
          )
          const rightNoDisabledTabs = tabs.filter(
            (item, itemIndex) => !item.disabled && itemIndex > index
          )

          if (rightNoDisabledTabs.length) {
            activeK = rightNoDisabledTabs[0].currentName
          } else if (leftNoDisabledTabs.length) {
            activeK =
              leftNoDisabledTabs[leftNoDisabledTabs.length - 1].currentName
          } else {
            activeK = newTabs[0].currentName
          }
        }
        activeKey.value = activeK
        emit('update:modelValue', activeK)
      }
      emit('on-tab-remove', tab.currentName)
      updateNav()
    }

    function handleClickDropdownItem() {
      if (props.autoCloseContextmenu) {
        closeContextMenu()
      }
    }

    function handleClickContextMenuOutside() {
      contextMenuVisible.value = false
    }

    // 禁用右键选择文本
    function handlePreventSelect(index, event) {
      const nav = navList.value[index]
      if (!nav || nav.disabled || !nav.contextMenu) {
        return
      }

      event.preventDefault()
    }

    function handleClickDropdownItem() {
      if (props.autoCloseContextmenu) {
        closeContextMenu()
      }
    }

    function showClose(item) {
      if (props.type === 'card') {
        if (item.closable !== null) {
          return item.closable
        } else {
          return props.closable
        }
      } else {
        return false
      }
    }

    // watch handlers

    watch(
      () => props.modelValue,
      (val) => {
        activeKey.value = focusedKey.value = val
      }
    )

    watch(activeKey, function (val) {
      focusedKey.value = val
      updateBar()
      updateStatus()
      tableList.forEach((item) => {
        item.table.handleOnVisibleChange(true)
      })
      nextTick(() => {
        scrollToActiveTab()
      })

      // update visibility
      const nextIndex = Math.max(getTabIndex(focusedKey.value), 0)
      updateVisibility(nextIndex)
    })

    onMounted(function () {
      showSlot.value = slots.extra !== undefined
      observer = elementResizeDetectorMaker()
      observer.listenTo(navWrap.value, handleResize)

      const hiddenParentNode = isInsideHiddenElement()
      if (hiddenParentNode) {
        mutationObserver = new MutationObserver(() => {
          if (hiddenParentNode.style.display !== 'none') {
            updateBar()
            mutationObserver.disconnect()
          }
        })

        mutationObserver.observe(hiddenParentNode, {
          attributes: true,
          childList: true,
          characterData: true,
          attributeFilter: ['style'],
        })
      }

      handleTabKeyboardSelect(true)
      updateVisibility(getTabIndex(activeKey.value))
    })

    onBeforeUnmount(function () {
      observer.removeListener(navWrap.value, handleResize)
      if (mutationObserver) {
        mutationObserver.disconnect()
      }
    })

    provide('TabsInstance', instance);
    expose({
      updateNav,
      paneList,
      activeKey
    })

    return function () {
      return (
        <>
          <div ref={tabsWrap} class={classes.value}>
            <div class={`${prefixCls}-bar`}>
              {showSlot.value && (
                <div class={`${prefixCls}-nav-right`}>
                  {
                    slots?.extra?.() || null
                  }
                </div>
              )}

              <div
                class={`${prefixCls}-nav-container`}
                tabindex="0"
                ref={navContainer}
                onKeydown={handleTabKeyNavigation}
                onKeydownSpacePrevent={() => handleTabKeyboardSelect(false)}
              >
                <div
                  ref={navWrap}
                  class={[
                    prefixCls + '-nav-wrap',
                    scrollable.value ? prefixCls + '-nav-scrollable' : '',
                  ]}
                >
                  <span
                    class={[
                      prefixCls + '-nav-prev',
                      scrollable.value
                        ? ''
                        : prefixCls + '-nav-scroll-disabled',
                    ]}
                    onClick={scrollPrev}
                  >
                    <Icon type="arrow-left"></Icon>
                  </span>
                  <span
                    class={[
                      prefixCls + '-nav-next',
                      scrollable.value
                        ? ''
                        : prefixCls + '-nav-scroll-disabled',
                    ]}
                    onClick={scrollNext}
                  >
                    <Icon type="arrow"></Icon>
                  </span>
                  <div
                    ref={navScroll}
                    class={[prefixCls + '-nav-scroll']}
                    onDOMMouseScroll={handleScroll}
                    onMousewheel={handleScroll}
                  >
                    <div
                      ref={nav}
                      class={[prefixCls + '-nav']}
                      style={navStyle}
                    >
                      <div
                        class={barClasses.value}
                        style={barStyle.value}
                      ></div>
                      {navList.value.map(function (item, index) {
                        return (
                          <div
                            class={tabCls(item)}
                            key={index}
                            onClick={() => handleChange(index)}
                            onDblclick={() => handleDblclick(index)}
                            onContextmenuStop={($event) =>
                              handleContextmenu(index, $event)
                            }
                            onSelectstartStop={($event) =>
                              handlePreventSelect(index, $event)
                            }
                            draggable={props.draggable}
                            onDragstart={($event) => handleDrag(index, $event)}
                            onDrop={($event) => handleDrop(index, $event)}
                            onDragover={withModifiers(() => true, ['prevent'])}
                          >
                            {
                              item.icon !== '' && <Icon type={item.icon} />
                            }


                            {
                              item.labelType === 'function' ?
                                item.label() : item.label
                            }

                            {
                              showClose(item) && <Icon
                                class={[prefixCls + '-close']}
                                type={arrowType.value}
                                custom={customArrowType.value}
                                size={arrowSize.value}
                                onClick={withModifiers(() => handleRemove(index), ['stop'])}
                              ></Icon>
                            }

                          </div>
                        )
                      })}
                    </div>
                  </div>
                </div>
              </div>
            </div>


            <div
              class={contentClasses.value}
              style={contentStyle.value}
              ref={panes}
            >
              {
                slots.default()
              }
            </div>

            <div class={`${prefixCls}-context-menu`} style={contextMenuStyles}>
              <Dropdown
                trigger="custom"
                visible={contextMenuVisible.value}
                transfer
                on-on-click={handleClickDropdownItem}
                on-on-clickoutside={handleClickContextMenuOutside}
              >
                {{
                  list: () => <DropdownMenu>{
                    slots?.contextMenu?.()
                  }</DropdownMenu>
                }}
              </Dropdown>
            </div>
          </div>
        </>
      )
    }
  },
})
</script>
